import json

from django.db import transaction
from django.utils import timezone
from rest_framework import serializers

from sysreptor import signals as sysreptor_signals
from sysreptor.pentests.collab.text_transformations import CollabStr, SelectionRange
from sysreptor.pentests.models import (
    CollabEvent,
    CollabEventType,
    Comment,
    CommentAnswer,
    FindingTemplate,
    Language,
    PentestFinding,
    PentestProject,
    ProjectMemberInfo,
    ProjectMemberRole,
    ProjectType,
    ReportSection,
    ReviewStatus,
    SourceEnum,
    UploadedImage,
    UploadedTemplateImage,
)
from sysreptor.pentests.serializers.common import ErrorMessageSerializer
from sysreptor.pentests.serializers.project_type import ProjectTypeRelatedField
from sysreptor.users.serializers import PentestUserSerializer, RelatedUserSerializer
from sysreptor.utils.fielddefinition.serializers import serializer_from_definition
from sysreptor.utils.fielddefinition.types import FieldDataType
from sysreptor.utils.fielddefinition.utils import (
    HandleUndefinedFieldsOptions,
    check_definitions_compatible,
    ensure_defined_structure,
    get_field_value_and_definition,
)
from sysreptor.utils.history import bulk_create_with_history, history_context
from sysreptor.utils.utils import find_index, omit_items


class TextRangeSerializer(serializers.Serializer):
    from_ = serializers.IntegerField(source='from_', min_value=0)
    to = serializers.IntegerField(min_value=0)

    def get_fields(self):
        fields = super().get_fields()
        fields['from'] = fields.pop('from_')
        return fields

    def validate(self, attrs):
        try:
            value = SelectionRange.from_dict(attrs)
        except ValueError as ex:
            raise serializers.ValidationError('Invalid text range') from ex
        if value.empty:
            raise serializers.ValidationError('Text range must not be empty')
        return value


class CommentAnswerSerializer(serializers.ModelSerializer):
    user = PentestUserSerializer(read_only=True)

    class Meta:
        model = CommentAnswer
        fields = ['id', 'created', 'updated', 'user', 'text']

    def create(self, validated_data):
        return super().create(validated_data | {
            'comment': self.context['comment'],
            'user': self.context['request'].user,
        })


class CommentSerializer(serializers.ModelSerializer):
    user = PentestUserSerializer(read_only=True)
    answers = CommentAnswerSerializer(many=True, read_only=True)
    text_range = TextRangeSerializer(allow_null=True)
    path = serializers.CharField(source='path_absolute', read_only=True)

    class Meta:
        model = Comment
        fields = [
            'id', 'created', 'updated', 'user', 'status', 'text',
            'path', 'text_range', 'text_original',
            'answers',
        ]
        read_only_fields = ['text_range', 'text_original']


class CommentCreateSerializer(CommentSerializer):
    path = serializers.CharField(source='path_absolute', read_only=False)

    class Meta(CommentSerializer.Meta):
        read_only_fields = omit_items(CommentSerializer.Meta.read_only_fields, ['text_range'])

    def validate_path(self, path_absolute: str):
        path_parts = path_absolute.split('.')
        if len(path_parts) < 4 or path_parts[0] not in ['findings', 'sections'] or path_parts[2] != 'data':
            raise serializers.ValidationError('Invalid path')

        obj = None
        if path_parts[0] == 'findings':
            obj = next(filter(lambda f: str(f.finding_id) == path_parts[1], self.context['project'].findings.all()), None)
        elif path_parts[0] == 'sections':
            obj = next(filter(lambda s: str(s.section_id) == path_parts[1], self.context['project'].sections.all()), None)
        if not obj:
            raise serializers.ValidationError('Invalid path')

        try:
            get_field_value_and_definition(data=obj.data, definition=obj.field_definition, path=path_parts[3:])
        except KeyError as ex:
            raise serializers.ValidationError('Invalid path') from ex

        return obj, '.'.join(path_parts[2:]), path_absolute

    def create(self, validated_data):
        from sysreptor.pentests.collab.consumer_base import rebase_selection

        obj, path, path_absolute = validated_data.pop('path_absolute')
        text_range = validated_data.pop('text_range')
        _, value, definition = get_field_value_and_definition(data=obj.data, definition=obj.field_definition, path=path.split('.')[1:])

        if text_range and definition.type in [FieldDataType.MARKDOWN] and isinstance(value, str):
            if version := float(self.context.get('version') or 0):
                # Rebase text_range to current version
                text_range, version = rebase_selection(
                    selection=text_range,
                    over_events=CollabEvent.objects.filter(related_id=self.context['project'].id).filter(path=path_absolute),
                    version=version,
                )
            value = CollabStr(value)
            if text_range.empty or text_range.to > len(value):
                raise serializers.ValidationError('Invalid text range')
            validated_data |= {
                'text_range': text_range,
                'text_original': value[text_range.anchor:text_range.head],
            }

        return super().create(validated_data | {
            'user': self.context.get('user') or getattr(self.context.get('request'), 'user', None),
            'path': path,
            'finding': obj if isinstance(obj, PentestFinding) else None,
            'section': obj if isinstance(obj, ReportSection) else None,
        })


class CommentResolveSerializer(CommentSerializer):
    class Meta(CommentSerializer.Meta):
        read_only_fields = omit_items(CommentSerializer.Meta.fields, ['status'])


class ReportSectionListSerializer(serializers.ListSerializer):
    def to_representation(self, data):
        sections = list(data.all() if isinstance(data, ReportSection.objects.__class__) else data)
        if sections:
            section_order = [s.get('id') for s in sections[0].project_type.report_sections]
            sections = sorted(sections, key=lambda s: find_index(section_order, str(s.section_id), 1000))
        return super().to_representation(sections)


class ReportSectionSerializer(serializers.ModelSerializer):
    id = serializers.CharField(source='section_id', read_only=True)
    project = serializers.PrimaryKeyRelatedField(read_only=True)
    project_type = ProjectTypeRelatedField(source='project.project_type_id', read_only=True)
    label = serializers.CharField(source='section_label', read_only=True)
    fields = serializers.ListField(source='section_fields', child=serializers.CharField(), read_only=True)
    assignee = RelatedUserSerializer(required=False, allow_null=True)

    class Meta:
        model = ReportSection
        fields = [
            'id', 'created', 'updated', 'label', 'fields', 'project', 'project_type',
            'language', 'assignee', 'status',
        ]
        list_serializer_class = ReportSectionListSerializer

    def get_fields(self):
        fields = super().get_fields()
        data_field = serializers.DictField()
        if self.instance and isinstance(self.instance, ReportSection):
            data_field = serializer_from_definition(definition=self.instance.field_definition, **self.get_extra_kwargs().get('data', {}))
        return fields | {
            'data': data_field,
        }

    def get_user(self):
        return self.context.get('user') or getattr(self.context.get('request'), 'user', None)

    def validate_status(self, value):
        if self.instance and not ((user := self.get_user()) and user.is_admin):
            ReviewStatus.validate_transition(self.instance.status, value, raise_exception=True)
        return value

    def update(self, instance, validated_data):
        instance.update_data(validated_data.pop('data', {}))
        return super().update(instance, validated_data)


class PentestFindingSerializer(serializers.ModelSerializer):
    id = serializers.UUIDField(source='finding_id', read_only=True)
    project = serializers.PrimaryKeyRelatedField(read_only=True)
    project_type = ProjectTypeRelatedField(source='project.project_type_id', read_only=True)
    template = serializers.PrimaryKeyRelatedField(read_only=True, source='template_id')
    assignee = RelatedUserSerializer(required=False, allow_null=True, default=serializers.CreateOnlyDefault(serializers.CurrentUserDefault()))

    class Meta:
        model = PentestFinding
        fields = [
            'id', 'created', 'updated', 'project', 'project_type',
            'language', 'template', 'assignee', 'status', 'order',
        ]
        read_only_fields = ['order']

    def get_fields(self):
        data_field = serializers.DictField()
        if self.context.get('project'):
            data_field = serializer_from_definition(definition=self.context['project'].project_type.finding_fields_obj, **self.get_extra_kwargs().get('data', {}))
        return super().get_fields() | {
            'data': data_field,
        }

    def get_user(self):
        return self.context.get('user') or getattr(self.context.get('request'), 'user', None)


    def validate_status(self, value):
        if self.instance and not ((user := self.get_user()) and user.is_admin):
            ReviewStatus.validate_transition(self.instance.status, value, raise_exception=True)
        return value

    def create(self, validated_data, handle_undefined=HandleUndefinedFieldsOptions.FILL_DEFAULT):
        data = ensure_defined_structure(
            value=validated_data.pop('data', {}),
            definition=self.context['project'].project_type.finding_fields_obj,
            handle_undefined=handle_undefined,
        )
        return PentestFinding.objects.create(
            project=self.context['project'],
            data=data,
            **validated_data,
        )

    def update(self, instance, validated_data):
        instance.update_data(validated_data.pop('data', {}))
        return super().update(instance, validated_data)


class PentestFindingFromTemplateSerializer(PentestFindingSerializer):
    template = serializers.PrimaryKeyRelatedField(queryset=FindingTemplate.objects.select_related('main_translation').all(), required=True, allow_null=False, source='template_id')
    template_language = serializers.ChoiceField(choices=Language.choices, required=False, allow_null=True, write_only=True)

    class Meta(PentestFindingSerializer.Meta):
        fields = PentestFindingSerializer.Meta.fields + ['template_language']
        read_only_fields = ['assignee', 'status']
        extra_kwargs = {'data': {'required': False}}

    def validate(self, attrs):
        if attrs.get('template_language') and not any(map(lambda tr: tr.language == attrs['template_language'], attrs['template_id'].translations.all())):
            raise serializers.ValidationError('Language does not exist in template')
        return super().validate(attrs)

    def update_image_references(self, data, name_old, name_new):
        # Update image references via simple string manipulation
        data_str = json.dumps(data)
        data_str = data_str.replace(f'/images/name/{name_old}', f'/images/name/{name_new}')
        return json.loads(data_str)

    def template_images_to_project_images(self, template, data):
        template_images = list(template.images.all())
        new_project_images = []
        if template_images:
            project_image_names = list(self.context['project'].images.values_list('name', flat=True))
            for ti in template_images:
                name = ti.name
                # Detect image name conflicts (same image name exists in project and template)
                if name in project_image_names:
                    while name in project_image_names + [i.name for i in template_images]:
                        name = UploadedTemplateImage.objects.randomize_name(ti.name)
                    # Change image references in template fields
                    data = self.update_image_references(data=data, name_old=ti.name, name_new=name)

                new_project_images.append(UploadedImage(
                    linked_object=self.context['project'],
                    name=name,
                    name_hash=UploadedImage.hash_name(name),
                    file=ti.file,
                ))
        return new_project_images, data

    @transaction.atomic()
    @history_context()
    def create(self, validated_data):
        template = validated_data.pop('template_id')
        template_language = validated_data.pop('template_language', None)
        translation = next(filter(lambda tr: tr.language == template_language, template.translations.all()), None) or template.main_translation
        data = translation.get_data(inherit_main=True)

        new_project_images, data = self.template_images_to_project_images(template, data)
        finding = super().create(validated_data | {
            'template_id': template.id,
            'data': data | validated_data.pop('data', {}),
        }, handle_undefined=HandleUndefinedFieldsOptions.FILL_NONE)
        bulk_create_with_history(UploadedImage, new_project_images)
        FindingTemplate.objects.filter(id=template.id).increment_usage_count()
        return finding


class ProjectMemberInfoSerializer(serializers.ModelSerializer):
    class Meta:
        model = ProjectMemberInfo
        fields = ['roles']

    def __init__(self, user_serializer=PentestUserSerializer, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.user_serializer = user_serializer

    def get_related_user_serializer(self):
        s = RelatedUserSerializer(user_serializer=self.user_serializer)
        s.bind('user', self)
        return s

    def to_representation(self, instance):
        return self.get_related_user_serializer().to_representation(instance.user) | \
            super().to_representation(instance)

    def to_internal_value(self, data):
        return super().to_internal_value(data) | {
            'user': self.get_related_user_serializer().to_internal_value(data),
        }


class ImportedProjectMemberInfoSerializer(serializers.ModelSerializer):
    roles = serializers.ListField(child=serializers.CharField(), allow_empty=True)

    class Meta(PentestUserSerializer.Meta):
        fields = omit_items(PentestUserSerializer.Meta.fields, ['username']) + ['roles']
        extra_kwargs = {
            'id': {'read_only': False},
        }


class ImportedProjectMemberInfoListSerializer(serializers.ListSerializer):
    child = ImportedProjectMemberInfoSerializer()

    def update(self, instance, validated_data):
        updated = []
        for d in validated_data:
            i = next(filter(lambda e: str(e.get('id')) == str(d.get('id')), instance), {})
            updated.append(i | d)
        return updated


class PentestProjectShortSerializer(serializers.ModelSerializer):
    project_type = ProjectTypeRelatedField()

    members = ProjectMemberInfoSerializer(many=True, required=False)
    imported_members = ImportedProjectMemberInfoListSerializer(required=False)

    copy_of = serializers.PrimaryKeyRelatedField(read_only=True)

    details = serializers.HyperlinkedIdentityField(view_name='pentestproject-detail', read_only=True)
    findings = serializers.HyperlinkedIdentityField(view_name='finding-list', lookup_url_kwarg='project_pk', read_only=True)
    sections = serializers.HyperlinkedIdentityField(view_name='section-list', lookup_url_kwarg='project_pk', read_only=True)
    notes = serializers.HyperlinkedIdentityField(view_name='projectnotebookpage-list', lookup_url_kwarg='project_pk', read_only=True)
    images = serializers.HyperlinkedIdentityField(view_name='uploadedimage-list', lookup_url_kwarg='project_pk', read_only=True)

    class Meta:
        model = PentestProject
        fields = [
            'id', 'created', 'updated',
            'name', 'project_type', 'language', 'tags', 'readonly', 'source', 'copy_of', 'override_finding_order',
            'members', 'imported_members',
            'details', 'findings', 'sections', 'notes', 'images',
        ]
        read_only_fields = ['readonly']


class PentestProjectDetailSerializer(PentestProjectShortSerializer):
    sections = ReportSectionSerializer(many=True, read_only=True)
    findings = PentestFindingSerializer(many=True, read_only=True)
    force_change_project_type = serializers.BooleanField(required=False, default=False, write_only=True)

    class Meta(PentestProjectShortSerializer.Meta):
        fields = PentestProjectShortSerializer.Meta.fields + ['force_change_project_type']

    def validate_project_type(self, value):
        if self.instance and self.instance.project_type != value and not self.initial_data.get('force_change_project_type'):
            res_finding = check_definitions_compatible(self.instance.project_type.finding_fields_obj, value.finding_fields_obj, path=('finding_fields',))
            res_report = check_definitions_compatible(self.instance.project_type.all_report_fields_obj, value.all_report_fields_obj, path=('report_fields',))
            if not res_finding[0] or not res_report[0]:
                raise serializers.ValidationError(['Designs have incompatible field definitions. Converting might result in data loss.'] + res_report[1] + res_finding[1])

        return value

    @transaction.atomic
    @history_context()
    def create(self, validated_data):
        project_type_original = validated_data.pop('project_type')
        project_type = project_type_original.copy(linked_user=None, source=SourceEnum.SNAPSHOT, usage_count=1)
        validated_data.pop('force_change_project_type')
        members = validated_data.pop('members', [])

        project = super().create(validated_data | {
            'project_type': project_type,
            'language': project_type.language,
            'unknown_custom_fields': ensure_defined_structure(
                value={
                    'title': validated_data.get('name', 'Report Title'),
                },
                definition=project_type.all_report_fields_obj,
                handle_undefined=HandleUndefinedFieldsOptions.FILL_DEFAULT,
            ),
            'skip_post_create_signal': True,
        })

        # add current user as member
        if not any(map(lambda m: m.get('user') == self.context['request'].user, members)):
            members.append({'user': self.context['request'].user, 'roles': ProjectMemberRole.default_roles})
        project.set_members([ProjectMemberInfo(**m) for m in members], new=True)

        project_type.linked_project = project
        project_type.save(update_fields=['linked_project'])
        ProjectType.objects.filter(id=project_type_original.id).increment_usage_count()
        sysreptor_signals.post_create.send(sender=project.__class__, instance=project)

        return project

    def update(self, instance, validated_data):
        members = validated_data.pop('members', None)
        if (imported_members := validated_data.get('imported_members')) is not None:
            validated_data['imported_members'] = self.fields['imported_members'].update(instance.imported_members, imported_members)
        if (project_type := validated_data.get('project_type')) and instance.project_type != project_type and project_type.linked_project != instance:
            ProjectType.objects.filter(id=project_type.id).increment_usage_count()
            validated_data['project_type'] = project_type.copy(
                linked_project=instance,
                linked_user=None,
                source=SourceEnum.SNAPSHOT,
                usage_count=1)

        instance = super().update(instance, validated_data)
        if members is not None:
            instance.set_members(members=[ProjectMemberInfo(**m) for m in members])
        return instance


class PentestFindingSortSerializer(serializers.ModelSerializer):
    id = serializers.UUIDField(source='finding_id')

    class Meta:
        model = PentestFinding
        fields = ['id', 'order']

    def validate_id(self, value):
        if not next(filter(lambda f: f.finding_id == value, self.parent.instance), None):
            raise serializers.ValidationError('Invalid finding id')
        return value


class PentestFindingSortListSerializer(serializers.ListSerializer):
    child = PentestFindingSortSerializer()

    def send_collab_event(self, instances):
        from sysreptor.pentests.consumers import send_collab_event_project

        time = max([f.updated for f in instances] or [timezone.now()])
        send_collab_event_project(CollabEvent.objects.create(
            related_id=self.context['project'].id,
            type=CollabEventType.SORT,
            path='findings',
            created=time,
            version=time.timestamp(),
            data={
                'sort': self.__class__(instances).data,
            },
        ))

    def update(self, instance, validated_data):
        missing_findings = []
        for finding in instance:
            if data := next(filter(lambda d: finding.finding_id == d.get('finding_id'), validated_data), None):
                finding.order = data.get('order')
            else:
                missing_findings.append(finding)

        PentestFinding.objects.update_order(instance, missing_findings)
        self.send_collab_event(instance)
        return instance


class PentestProjectReadonlySerializer(serializers.ModelSerializer):
    class Meta:
        model = PentestProject
        fields = ['readonly']


class PentestProjectCopySerializer(serializers.ModelSerializer):
    project_type = ProjectTypeRelatedField(required=False)

    class Meta:
        model = PentestProject
        fields = ['id', 'name', 'project_type']
        extra_kwargs = {'name': {'required': False}}

    def update(self, instance, validated_data):
        return instance.copy(
            name='Copy of ' + instance.name,
            source=SourceEnum.CREATED,
            **validated_data,
        )


class PentestProjectCheckSerializer(serializers.ModelSerializer):
    messages = ErrorMessageSerializer(many=True, read_only=True)

    class Meta:
        model = PentestProject
        fields = ['messages']

    def update(self, instance, validated_data):
        return {
            'messages': instance.perform_checks(),
        }


class CustomizeProjectTypeSerializer(serializers.ModelSerializer):
    class Meta:
        model = PentestProject
        fields = ['project_type']
        read_only_fields = ['project_type']

    def update(self, instance, validated_data):
        instance.project_type = instance.project_type.copy(
            name='Customization of ' + instance.project_type.name,
            source=SourceEnum.CUSTOMIZED,
            linked_project=instance,
            linked_user=None,
            usage_count=1)
        instance.save()
        return instance


class PreviewPdfOptionsSerializer(serializers.Serializer):
    report_template = serializers.CharField(required=False, allow_null=True, allow_blank=True, write_only=True)
    report_styles = serializers.CharField(required=False, allow_null=True, allow_blank=True, write_only=True)


class PublishPdfOptionsSerializer(serializers.Serializer):
    password = serializers.CharField(required=False, allow_null=True, allow_blank=True, write_only=True)


class Md2HtmlOptionsSerializer(serializers.Serializer):
    pass
